package crmdna.common;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.text.DecimalFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.regex.Pattern;

import javax.mail.MessagingException;
import javax.servlet.http.HttpServletRequest;

import com.google.appengine.api.users.User;

import crmdna.common.APIResponse.Status;
import crmdna.common.config.ConfigCRMDNA;
import crmdna.email.EmailProp;
import crmdna.email.GAEEmail;

public class Utils {

	public enum Currency {
		SGD, USD, INR, MYR, AUD, GBP
	}

	public enum PaypalErrorType {
		PAYPAL_SET_EXPRESS_CHECKOUT_FAILURE, PAYPAL_GET_EXPRESS_CHECKOUT_FAILURE, PAYPAL_DO_EXPRESS_CHECKOUT_FAILURE
	}

	public static Set<String> getQSTags(String... fields) {
		// creates all combinations of 3 consecutive character as a set
		// eg: if invoked with getQSTags("sathya", "thilakan") the set
		// will be populated as: sat, ath, thy, hya, thi, hil, ila, aka, kan

		Set<String> set = new TreeSet<>();

		for (int i = 0; i < fields.length; i++) {
			if (fields[i] == null)
				continue;

			fields[i] = fields[i].toLowerCase();
			if (fields[i].length() < 3)
				continue;

			for (int beginIndex = 0; beginIndex < fields[i].length() - 2; beginIndex++) {
				set.add(fields[i].substring(beginIndex, beginIndex + 3));
			}
		}

		return set;
	}

	public static boolean closeEnough(String s1, String s2) {

		if ((s1 == null) || (s2 == null))
			return false;

		s1 = s1.toLowerCase().replaceAll(" ", "");
		s2 = s2.toLowerCase().replaceAll(" ", "");

		// if less than or equal to 3 char, entire string should match
		if (s1.length() <= 3)
			return s1.equals(s2);

		// if first 3 chars match return true
		if (s1.substring(0, 3).equals(s2.substring(0, 3)))
			return true;

		Set<String> s1QSTags = getQSTags(s1);
		Set<String> s2QSTags = getQSTags(s2);

		s1QSTags.retainAll(s2QSTags);

		return s1QSTags.size() > Math.min(s1.length(), s1.length()) / 3;
	}

	public static boolean isValidEmailAddress(String email) {
		if (null == email)
			return false;

		boolean stricterFilter = true;
		String stricterFilterString = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}";
		String laxString = ".+@.+\\.[A-Za-z]{2}[A-Za-z]*";
		String emailRegex = stricterFilter ? stricterFilterString : laxString;
		java.util.regex.Pattern p = java.util.regex.Pattern.compile(emailRegex);
		java.util.regex.Matcher m = p.matcher(email);
		return m.matches();
	}

	public static boolean isPresentInListCaseInsensitive(List<String> list,
			String s) {

		if ((null == list) || (null == s))
			return false;

		for (String element : list) {
			if (element.equalsIgnoreCase(s))
				return true;
		}

		return false;
	}

	public static String getLoginEmail(User user) {
		if (null == user)
			return null;

		return user.getEmail();
	}

	public static void throwIncorrectSpecException(String message) {
		throw new APIException(Status.ERROR_RESOURCE_INCORRECTLY_SPECIFIED,
				message);
	}

	public static void throwNotFoundException(String message) {
		throw new APIException(Status.ERROR_RESOURCE_DOES_NOT_EXIST, message);
	}

	public static void ensureValidPhoneNumber(String phoneNumber) {
		//TODO: remove code duplication in isValidPhoneNumber function
		final String phoneNumberRegex = "\\+(9[976]\\d|8[987530]\\d|6[987]\\d|5[90]\\d|42\\d|3[875]\\d|"
				+ "2[98654321]\\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|"
				+ "4[987654310]|3[9643210]|2[70]|7|1)\\d{1,14}$";

		if ((phoneNumber == null) || (phoneNumber.equals("")))
			throw new APIException(Status.ERROR_RESOURCE_INCORRECTLY_SPECIFIED,
					"Phone number cannot be null or empty");

		if (phoneNumber.charAt(0) != '+')
			throw new APIException(Status.ERROR_RESOURCE_INCORRECTLY_SPECIFIED,
					"First character of phone number should be +");

		if (!phoneNumber.matches(phoneNumberRegex))
			throw new APIException(Status.ERROR_RESOURCE_INCORRECTLY_SPECIFIED,
					"Phone number is invalid");
	}

	public static boolean isValidPhoneNumber(String phoneNumber) {
		final String phoneNumberRegex = "\\+(9[976]\\d|8[987530]\\d|6[987]\\d|5[90]\\d|42\\d|3[875]\\d|"
				+ "2[98654321]\\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|"
				+ "4[987654310]|3[9643210]|2[70]|7|1)\\d{1,14}$";

		if ((phoneNumber == null) || phoneNumber.equals(""))
			return false;

		if (phoneNumber.charAt(0) != '+')
			return false;

		if (!phoneNumber.matches(phoneNumberRegex))
			return false;
		
		return true;
	}

	public static void ensureValidEmail(String email) {
		if ((email == null) || (email.equals("")))
			throw new APIException(Status.ERROR_RESOURCE_INCORRECTLY_SPECIFIED,
					"Email cannot be null or empty");

		boolean valid = Utils.isValidEmailAddress(email);

		if (valid == false)
			throw new APIException(Status.ERROR_RESOURCE_INCORRECTLY_SPECIFIED,
					"Email [" + email + "] is invalid");
	}

	public static void ensureValidUrl(String url) {

		try {
			new URL(url);
		} catch (MalformedURLException e) {
			Utils.throwIncorrectSpecException("URL [" + url + "] is invalid");
		}
	}

	public static long safeParseAsLong(String s) {
		try {
			long l = Long.parseLong(s);
			return l;
		} catch (NumberFormatException e) {
			throw new APIException(Status.ERROR_RESOURCE_INCORRECTLY_SPECIFIED,
					"Cannot parse [" + s + "] as long");
		}
	}

	public static boolean canParseAsLong(String s) {
		try {
			safeParseAsLong(s);
			return true;
		} catch (Exception ex) {
			return false;
		}
	}

	public static int safeParseAsInt(String s) {
		try {
			int i = Integer.parseInt(s);
			return i;
		} catch (NumberFormatException e) {
			throw new APIException(Status.ERROR_RESOURCE_INCORRECTLY_SPECIFIED,
					"Cannot parse [" + s + "] as integer");
		}
	}

	public static double safeParseAsDouble(String s) {
		try {
			double d = Double.parseDouble(s);
			return d;
		} catch (NumberFormatException e) {
			throw new APIException(Status.ERROR_RESOURCE_INCORRECTLY_SPECIFIED,
					"Cannot parse [" + s + "] as double");
		}
	}

	public static void ensureFormatYYYYMMDD(long date) {

		boolean valid = isFormatInYYYYMMDD(date);
		if (!valid)
			throw new APIException(
					Status.ERROR_RESOURCE_INCORRECTLY_SPECIFIED,
					"["
							+ date
							+ "] is not valid. Date should be valid in YYYYMMDD format");
	}

	public static boolean isFormatInYYYYMMDD(long date) {
		String s = "" + date;

		if (s.length() != 8)
			return false;

		int year = Integer.parseInt(s.substring(0, 4));
		int month = Integer.parseInt(s.substring(4, 6));
		int day = Integer.parseInt(s.substring(6, 8));

		if ((year < 1970) || (year > 2050))
			return false;

		if ((month < 1) || (month > 12))
			return false;

		if ((day < 1) || (day > 31))
			return false;

		if (month == 2) {
			if (day > 29)
				return false;

			if ((day == 29) && (year % 4 != 0))
				return false;
		}

		if ((month == 4) || (month == 6) || (month == 9) || (month == 11))
			if (day == 31)
				return false;

		return true;
	}

	public static String toDDMMMYY(long dateYYYYMMDD) {
		ensureFormatYYYYMMDD(dateYYYYMMDD);

		String s = "" + dateYYYYMMDD;

		int year = Integer.parseInt(s.substring(2, 4));
		int month = Integer.parseInt(s.substring(4, 6));
		int day = Integer.parseInt(s.substring(6, 8));

		return day + " " + get3CharMonth(month) + " " + year;
	}

	public static String toDDMMM(long dateYYYYMMDD) {
		ensureFormatYYYYMMDD(dateYYYYMMDD);

		String s = "" + dateYYYYMMDD;

		int month = Integer.parseInt(s.substring(4, 6));
		int day = Integer.parseInt(s.substring(6, 8));

		return day + " " + get3CharMonth(month);
	}

	public static String get3CharMonth(int month) {
		if (1 == month)
			return "Jan";

		if (2 == month)
			return "Feb";

		if (3 == month)
			return "Mar";

		if (4 == month)
			return "Apr";

		if (5 == month)
			return "May";

		if (6 == month)
			return "Jun";

		if (7 == month)
			return "Jul";

		if (8 == month)
			return "Aug";

		if (9 == month)
			return "Sep";

		if (10 == month)
			return "Oct";

		if (11 == month)
			return "Nov";

		if (12 == month)
			return "Dec";

		throw new APIException(Status.ERROR_RESOURCE_INCORRECTLY_SPECIFIED,
				"Invalid month [" + month + "]");
	}

	public static String getDurationAsString(long startYYYYMMDD,
			long endYYYYMMDD) {
		ensureFormatYYYYMMDD(startYYYYMMDD);
		ensureFormatYYYYMMDD(endYYYYMMDD);

		if (startYYYYMMDD > endYYYYMMDD)
			throw new APIException(Status.ERROR_RESOURCE_INCORRECTLY_SPECIFIED,
					"Start date should not be greater than end date");

		if (startYYYYMMDD == endYYYYMMDD)
			return Utils.toDDMMMYY(startYYYYMMDD);

		String s = "" + startYYYYMMDD;
		int yearStart = Integer.parseInt(s.substring(0, 4));
		int monthStart = Integer.parseInt(s.substring(4, 6));
		int dayStart = Integer.parseInt(s.substring(6, 8));

		s = "" + endYYYYMMDD;
		int yearEnd = Integer.parseInt(s.substring(0, 4));
		int monthEnd = Integer.parseInt(s.substring(4, 6));

		if (yearStart == yearEnd) {
			if (monthStart == monthEnd) {
				return dayStart + " - " + toDDMMMYY(endYYYYMMDD);
			}

			return toDDMMM(startYYYYMMDD) + " - " + toDDMMMYY(endYYYYMMDD);
		}

		return toDDMMM(startYYYYMMDD) + " - " + toDDMMMYY(endYYYYMMDD);
	}

	public static String getDateDiff(long startTimeInSeconds,
			long endTimeInSeconds) {
		long diff = endTimeInSeconds - startTimeInSeconds;
		if (diff < 0)
			Utils.throwIncorrectSpecException("startTimeInSeconds greater than endTimeInSeconds");

		if (diff < 60)
			return diff + " seconds";

		if (diff < 3600) {
			long min = Math.round(diff / 60.0);
			return min + " minute(s)";
		}

		if (diff < 3600 * 24) {
			long hours = Math.round(diff / 3600.0);
			return hours + " hour(s)";
		}

		long days = Math.round(diff / 3600.0 / 24.0);
		return days + " day(s)";
	}

	public static String getUrl(String baseUrl, Map<String, Object> queryParams) {
		Utils.ensureValidUrl(baseUrl);

		if (queryParams == null)
			return baseUrl;

		// sort query params by key
		Map<String, Object> treeMap = new TreeMap<>();
		treeMap.putAll(queryParams);

		queryParams = treeMap;

		StringBuilder builder = new StringBuilder(baseUrl);
		if (!baseUrl.contains("?"))
			builder.append("?");
		else
			builder.append("&");

		for (String key : queryParams.keySet()) {
			try {
				final String ENCODING_SCHEME = "UTF-8";
				String encodedKey = URLEncoder.encode(key, ENCODING_SCHEME);
				String encodedValue = URLEncoder.encode(queryParams.get(key)
						.toString(), ENCODING_SCHEME);

				if (encodedValue.length() > 250)
					encodedValue = encodedValue.substring(0, 250);

				builder.append(encodedKey + "=" + encodedValue + "&");
			} catch (UnsupportedEncodingException e) {
				e.printStackTrace();

				throw new RuntimeException(
						"Unsupported Encoding Exception. Message: "
								+ e.getMessage());
			}
		}

		String url = builder.toString();

		// space should be encoded as %20, java encoder encodes it as a +
		url = url.replaceAll(Pattern.quote("+"), "%20");

		// should be valid - but just in case
		Utils.ensureValidUrl(url);

		return url;
	}

	public static void ensureNonNegative(double n) {
		if (n < 0)
			throwIncorrectSpecException("Specified number [" + n
					+ "] is negative");
	}

	public static int toYYYYMMDD(Date date) {
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(date);

		int year = calendar.get(Calendar.YEAR);
		int month = calendar.get(Calendar.MONTH) + 1;
		int day = calendar.get(Calendar.DAY_OF_MONTH);

		int yyyymmdd = day + month * 100 + year * 10000;
		return yyyymmdd;
	}

	public static Date toDate(int yyyymmdd) {

		if (!isFormatInYYYYMMDD(yyyymmdd)) {
			Utils.throwIncorrectSpecException("[" + yyyymmdd
					+ "] is not in yyymmdd format");
		}

		String s = yyyymmdd + "";

		int year = Integer.parseInt(s.substring(0, 4));
		int month = Integer.parseInt(s.substring(4, 6));
		int day = Integer.parseInt(s.substring(6, 8));

		Calendar calendar = Calendar.getInstance();
		calendar.set(year, month - 1, day);

		return calendar.getTime();
	}

	public static int getNumDays(Date former, Date later) {
		final int MILLI_SECONDS_IN_A_DAY = 3600 * 24 * 1000;

		int numDays = (int) (former.getTime() - later.getTime())
				/ MILLI_SECONDS_IN_A_DAY;

		return numDays;
	}

	public static String getFullName(String firstName, String lastName) {
		if ((firstName == null) && (lastName == null))
			return null;

		if (firstName == null)
			return lastName;

		if (lastName == null)
			return firstName;

		return firstName + " " + lastName;
	}

	public static String asCurrencyString(double d) {
		DecimalFormat df = new DecimalFormat("#.00");
		return df.format(d);
	}

	public static String urlEncode(String url) {
		try {
			url = URLEncoder.encode(url, "UTF-8");
		} catch (UnsupportedEncodingException e) {
			throw new APIException(Status.ERROR_OPERATION_NOT_ALLOWED,
					"Exception thrown by URLEncoder.encode for url [" + url
							+ "]");
		}

		url.replaceAll(Pattern.quote("+"), "%20");
		return url;
	}

	public static void ensureNotNullOrEmpty(String value, String errMessage) {
		if ((value == null) || value.equals(""))
			Utils.throwIncorrectSpecException(errMessage);
	}

	public static void ensureNonZero(long value, String errMessage) {
		if (value == 0)
			Utils.throwIncorrectSpecException(errMessage);
	}

	public static String csvEncode(String s) {
		if (s == null)
			return "";

		if (s.contains(","))
			return "\"" + s + "\"";

		return s;
	}

	public static String stackTraceToString_old(Throwable e) {
		StringWriter sw = new StringWriter();
		e.printStackTrace(new PrintWriter(sw));

		return sw.toString();
	}
	
	public static String stackTraceToString(Throwable e) {
		StringBuilder builder = new StringBuilder();
		
		StackTraceElement[] stElements = e.getStackTrace();
		
		for (int i = 0; i < stElements.length; i++) {
			builder.append(stElements[i].toString());
			builder.append("<br>");
		}				

		return builder.toString();
	}

	// TODO: move this to incidents package later
	public static void sendAlertEmailToDevTeam(String client, Exception ex,
			HttpServletRequest req, String login)
			throws UnsupportedEncodingException, MessagingException {

		if ((ex == null) || (req == null))
			return;

		// to address
		EmailProp emailProp = new EmailProp();
		emailProp.toEmailAddresses = ConfigCRMDNA.get().toProp().devTeamEmails;

		if (emailProp.toEmailAddresses.size() == 0)
			emailProp.toEmailAddresses.add(crmdna.user.User.SUPER_USER);

		// subject
		emailProp.subject = "Unhandled exception for [" + client + "]: "
				+ ex.getMessage();

		// body
		StringBuilder builder = new StringBuilder();
		builder.append("<br><i>Logged in user:</i> " + login + "<br><br>");
		builder.append("<i>Timestamp:</i> " + new Date() + "<br><br>");

		builder.append("<i>Request:</i> " + req.getRequestURI() + "<br><br>");

		@SuppressWarnings("unchecked")
		Enumeration<String> parameterNames = req.getParameterNames();

		if (parameterNames.hasMoreElements()) {
			builder.append("<i>Query parameters: </i><br>");
			while (parameterNames.hasMoreElements()) {
				String key = (String) parameterNames.nextElement();
				String value = req.getParameter(key);
				builder.append(key + ": " + value + "<br>");
			}
		}
		builder.append("<br>");
		
		@SuppressWarnings("unchecked")
		Enumeration<String> headerNames = req.getParameterNames();
		if (headerNames.hasMoreElements()) {
			builder.append("<i>Headers: </i><br>");
			while (headerNames.hasMoreElements()) {
				String key = (String) parameterNames.nextElement();
				String value = req.getParameter(key);
				builder.append(key + ": " + value + "<br>");
			}
		}

		builder.append("<br>");
		builder.append("<i>Exception message:</i> " + ex.getMessage());
		builder.append("<br><br>");
		builder.append("<i>Stack trace:</i><br>");
		builder.append(stackTraceToString(ex));

		emailProp.bodyHtml = builder.toString();

		GAEEmail.send(emailProp);
	}
	
	public static String sanitizePhoneNo(String phoneNo) {
		if (phoneNo == null)
			return null;
		
		//remove - and empty spaces
		phoneNo = phoneNo.replaceAll(Pattern.quote("-"), "");
		phoneNo = phoneNo.replaceAll(Pattern.quote(" "), "");
		
		//remove leading zeros
		phoneNo = phoneNo.replaceAll("^0+", "");
		
		if (phoneNo.equals(""))
			return null;		
		
		//if it does not contain + add it
		if ((phoneNo.length() > 1) && (phoneNo.charAt(0) != '+'))
			phoneNo = "+" + phoneNo;
		
		return phoneNo;		
	}
}
